package com.jprocms.module.cms.service.site;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.google.common.annotations.VisibleForTesting;
import com.jpro.framework.common.enums.UserTypeEnum;
import com.jpro.framework.common.pojo.CommonResult;
import com.jpro.framework.common.util.object.BeanUtils;
import com.jpro.framework.security.core.LoginUser;
import com.jpro.framework.security.core.util.SecurityFrameworkUtils;
import com.jpro.framework.tenant.core.context.TenantContextHolder;
import com.jpro.framework.tenant.core.util.TenantUtils;
import com.jpro.framework.web.config.WebProperties;
import com.jpro.module.system.api.permission.PermissionApi;
import com.jpro.module.system.api.permission.dto.SitePermissionRespDTO;
import com.jpro.module.system.api.user.AdminUserApi;
import com.jprocms.module.cms.api.dto.SiteConfigDto;
import com.jprocms.module.cms.api.dto.SiteDefTplDto;
import com.jprocms.module.cms.cache.SiteRedisCache;
import com.jprocms.module.cms.controller.admin.site.dto.SiteQueryDto;
import com.jprocms.module.cms.controller.admin.site.vo.SiteCreateReqVO;
import com.jprocms.module.cms.controller.admin.site.vo.SiteQueryReqVO;
import com.jprocms.module.cms.controller.admin.site.vo.SiteUpdateReqVO;
import com.jprocms.module.cms.convert.site.SiteConvert;
import com.jprocms.module.cms.dal.dataobject.site.SiteDO;
import com.jprocms.module.cms.dal.mysql.site.SiteMapper;
import com.jprocms.module.cms.dal.mysql.sitedetailtpl.SiteDetailTplMapper;
import com.jprocms.module.cms.dal.mysql.sitetpl.SiteTplMapper;
import com.jprocms.module.cms.enums.EnumIntCommonStatus;
import com.jprocms.module.cms.enums.EnumSiteCopyOp;
import com.jprocms.module.cms.framework.cache.CacheService;
import com.jprocms.module.cms.framework.tree.TreeServiceImpl;
import com.jprocms.module.cms.service.channel.ChannelService;
import com.jprocms.module.cms.service.globalconfig.GlobalConfigService;
import com.jprocms.module.cms.service.sitetpl.SiteTplService;
import com.jprocms.module.cms.service.sitetpl.TplService;
import com.jprocms.module.cms.util.CmsContextUtil;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.jpro.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.jpro.framework.common.util.collection.CollectionUtils.getMaxValue;
import static com.jprocms.module.cms.cache.BaseRedisCache.MYBATIS_CACHEPREFIX;
import static com.jprocms.module.cms.constant.CmsSysConstants.STATUS_CLOSE;
import static com.jprocms.module.cms.constant.CmsSysConstants.STATUS_OPEN;
import static com.jprocms.module.cms.dal.redis.RedisKeyConstants.SITE_CACHE_KEY;
import static com.jprocms.module.cms.enums.ErrorCodeConstants.*;
import static com.jprocms.module.cms.enums.LogRecordConstants.*;

/**
 * 站点 Service 实现类
 *
 * @author jprocms
 */
@Slf4j
@Service
@Validated
public class SiteServiceImpl extends TreeServiceImpl<SiteMapper, SiteDO> implements SiteService, CommandLineRunner {

    @Resource
    private SiteMapper siteMapper;
    @Resource
    private GlobalConfigService globalConfigService;
    @Resource
    private SiteTplMapper siteTplMapper;
    @Resource
    private SiteDetailTplMapper siteDetailTplMapper;
    @Resource
    private SiteTplService siteTplService;
    @Resource
    private TplService tplService;
    @Resource
    private ChannelService channelService;
    @Resource
    private CacheService cacheService;

    private WebProperties webProperties;
    private AdminUserApi adminUserApi;
    private PermissionApi permissionApi;
    /**
     * 站点访问目录列表缓存
     */
    @Getter
    private volatile Set<String> siteVisitPathCache = new HashSet<>();
    private volatile Set<SiteDO> siteCache = new HashSet<>();

    public SiteServiceImpl(WebProperties webProperties, AdminUserApi adminUserApi, PermissionApi permissionApi) {
        this.webProperties = webProperties;
        this.adminUserApi = adminUserApi;
        this.permissionApi = permissionApi;
    }

    @Override
    @LogRecord(type = CMS_SITE_TYPE, subType = CMS_SITE_CREATE_SUB_TYPE, bizNo = "{{#site.id}}", success = CMS_SITE_CREATE_SUCCESS)
    public Long createSite(SiteCreateReqVO createReqVO) throws IOException {
        // 校验站点单存在
        validateParentSite(createReqVO.getParentId(), null);
        // 校验站点（自己）
        validateSite(createReqVO.getParentId(), createReqVO.getName(), null);
        // 插入
        SiteDO site = SiteConvert.INSTANCE.convert(createReqVO);
        site.init();
        // 父站点不为空，则复制站点配置
        final SiteDO parentSiteDO;
        if (createReqVO.getParentId() != null && createReqVO.getParentId() != 0) {
            parentSiteDO = getSite(createReqVO.getParentId());
        }else{
            //复制默认站点配置
            parentSiteDO = getSite(1L);
        }
        site.setH5Solution(parentSiteDO.getH5Solution());
        site.setPcSolution(parentSiteDO.getPcSolution());
        site.setSiteConfig(parentSiteDO.getSiteConfig());
        site.setGlobalConfig(parentSiteDO.getGlobalConfig());
        //复制站点模板配置
        tplService.copySiteTplAndRes(parentSiteDO, site);
        //nodePath 父类统一处理
        super.save(site);
        final SiteConfigDto siteConfig = site.getSiteConfig();
        siteConfig.setId(site.getId());
        site.setSiteConfig(siteConfig);
        siteMapper.updateById(site);
        //复制栏目
        if (EnumSiteCopyOp.COPY_CHANNEL.getCopyOp().equals(createReqVO.getCopyOp())
                && createReqVO.getParentId() != null && createReqVO.getParentId() != 0) {
            channelService.copyChannels(createReqVO.getParentId(), site.getId());
        }
        //复制栏目和内容
        if (EnumSiteCopyOp.COPE_CHANNEL_CONTENT.getCopyOp().equals(createReqVO.getCopyOp())) {
            channelService.copyChannelsAndContents(createReqVO.getParentId(), site.getId());
        }
        // 3. 记录操作日志上下文
        LogRecordContext.putVariable("site", site);
        // 返回
        return site.getId();
    }

    @Override
    public Long createTenantSite(SiteCreateReqVO createReqVO, SiteDO refSite) throws IOException {
        // 校验站点单存在
        validateParentSite(createReqVO.getParentId(), null);
        // 校验站点（自己）
        validateSite(createReqVO.getParentId(), createReqVO.getName(), null);
        // 插入
        SiteDO site = SiteConvert.INSTANCE.convert(createReqVO);
        site.init();
        // 父站点不为空，则复制站点配置
        SiteDO parentSiteDO;
        if (createReqVO.getParentId() != null && createReqVO.getParentId() != 0) {
            parentSiteDO = getSite(createReqVO.getParentId());
        } else {
            TenantContextHolder.setTenantId(TenantContextHolder.DEF_TENANT_ID);
            parentSiteDO = getSite(1L);
        }
        TenantContextHolder.setTenantId(createReqVO.getTenantId());
        site.setH5Solution(parentSiteDO.getH5Solution());
        site.setPcSolution(parentSiteDO.getPcSolution());
        site.setSiteConfig(parentSiteDO.getSiteConfig());
        site.setGlobalConfig(parentSiteDO.getGlobalConfig());
        site.setTenantId(createReqVO.getTenantId());
        //复制站点模板配置
        tplService.copySiteTplAndRes(parentSiteDO, site);
        //创建租户的时候创建站点行为，需要切换数据源寻找默认站点复制站点资源模板
        if (refSite != null) {
            site.setH5Solution(refSite.getH5Solution());
            site.setPcSolution(refSite.getPcSolution());
            site.setGlobalConfig(refSite.getGlobalConfig());
            //复制站点模板配置
            tplService.copySiteTplAndRes(refSite, site);
        }
        //nodePath 父类统一处理
        super.save(site);
        //修改站点配置的id值
        if (site.getSiteConfig() != null) {
            site.getSiteConfig().setId(site.getId());
        }
        siteMapper.updateById(site);
        //复制栏目
        if (EnumSiteCopyOp.COPY_CHANNEL.getCopyOp().equals(createReqVO.getCopyOp())
                && createReqVO.getParentId() != null && createReqVO.getParentId() != 0) {
            channelService.copyChannels(createReqVO.getParentId(), site.getId());
        }
        //复制栏目和内容
        if (EnumSiteCopyOp.COPE_CHANNEL_CONTENT.getCopyOp().equals(createReqVO.getCopyOp())) {
            channelService.copyChannelsAndContents(createReqVO.getParentId(), site.getId());
        }
        // 返回
        return site.getId();
    }
    @Override
    @LogRecord(type = CMS_SITE_TYPE, subType = CMS_SITE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = CMS_SITE_UPDATE_SUCCESS)
    public void updateSite(SiteUpdateReqVO updateReqVO) {
        // 校验存在
        SiteDO oldSite = validateSiteExists(updateReqVO.getId());
        // 校验站点存在
        validateParentSite(updateReqVO.getParentId(), null);
        // 校验站点（自己）
        validateSite(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId());
        // 更新
        SiteDO updateObj = SiteConvert.INSTANCE.convert(updateReqVO);
//        siteMapper.updateById(updateObj);
        //调用父类重载方法更新nodepath
        updateById(updateObj);
        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldSite, SiteUpdateReqVO.class));
        LogRecordContext.putVariable("site", updateObj);
    }

    @Override
    @LogRecord(type = CMS_SITE_TYPE, subType = CMS_SITE_CONFIG_UPDATE_SUB_TYPE, bizNo = "{{#siteConfigDto.id}}", success = CMS_SITE_CONFIG_UPDATE_SUCCESS)
    public void updateSiteConfig(SiteConfigDto siteConfigDto) {
        // 校验存在
        validateSiteExists(siteConfigDto.getId());
        SiteDO updateObj = new SiteDO();
        //全量配置存储JSON
        updateObj.setId(siteConfigDto.getId());
        updateObj.setSiteConfig(siteConfigDto);
        if (siteConfigDto.getBase().getAllowPush() != null) {
            updateObj.setAllowPush(siteConfigDto.getBase().getAllowPush() ? STATUS_OPEN : STATUS_CLOSE);
        }
        siteMapper.updateById(updateObj);
        Long siteId = siteConfigDto.getId();
        LogRecordContext.putVariable("site", getSite(siteId));
    }

    public void clearSiteCache(Long siteId) {
        //删除二级缓存
        final Object siteCacheKey = cacheService.getCacheMapValue(SITE_CACHE_KEY, siteId.toString());
        if (siteCacheKey != null) {
            cacheService.deleteObject(MYBATIS_CACHEPREFIX + SiteRedisCache.CACHE_NAME + siteCacheKey.toString());
            cacheService.deleteCacheMapValue(SITE_CACHE_KEY, siteId.toString());
        }
    }

    @Override
    @LogRecord(type = CMS_SITE_TYPE, subType = CMS_SITE_DELETE_SUB_TYPE, bizNo = "{{#id}}", success = CMS_SITE_DELETE_SUCCESS)
    public void deleteSite(Long id) {
        // 校验存在
        SiteDO siteDO = validateSiteExists(id);
        // 删除
        siteMapper.deleteById(id);
        LogRecordContext.putVariable("site", siteDO);
    }

    private SiteDO validateSiteExists(Long id) {
        final SiteDO site = getSite(id);
        if (site == null) {
            throw exception(SITE_NOT_EXISTS);
        }
        return site;
    }

    /**
     * 校验父站点是否合法
     * <p>
     * 1. 不能设置自己为父站点
     * 2. 父站点不存在
     *
     * @param parentId 父站点编号
     * @param childId  当前菜单编号
     */
    @VisibleForTesting
    void validateParentSite(Long parentId, Long childId) {
        if (parentId == null || parentId == 0) {
            return;
        }
        // 不能设置自己为父site
        if (parentId.equals(childId)) {
            throw exception(SITE_PARENT_ERROR);
        }
        SiteDO site = getSite(parentId);
        // 父site不存在
        if (site == null) {
            throw exception(SITE_NOT_EXISTS);
        }
    }

    /**
     * 校验菜单是否合法
     * <p>
     * 1. 校验相同父菜单编号下，是否存在相同的菜单名
     *
     * @param name     站点名字
     * @param parentId 父站点编号
     * @param id       站点ID
     */
    @VisibleForTesting
    void validateSite(Long parentId, String name, Long id) {
        SiteDO site = siteMapper.selectByParentIdAndName(parentId, name);
        if (site == null) {
            return;
        }
        if (!site.getId().equals(id)) {
            throw exception(SITE_NAME_DUPLICATE);
        }
    }

    @Override
    public SiteDO getSite(Long id) {
        return siteMapper.selectById(id);
    }

    @Override
    public List<SiteDO> getSiteList(Collection<Long> ids) {
        return siteMapper.selectBatchIds(ids);
    }

    @Override
    public List<SiteDO> getSiteList(SiteQueryReqVO siteQueryReqVO) {
        return siteMapper.selectSiteList(siteQueryReqVO);
    }

    @Override
    public List<SiteDO> getSiteList(Integer globalConfigId) {
        return siteMapper.selectList(SiteDO::getGlobalConfig, globalConfigId);
    }

    @Override
    public SiteDO getCurrentSite(SiteQueryDto siteQueryDto, HttpServletRequest request) {
        SiteDO site = null;
        //后台优先header
        Long siteId = siteQueryDto.getSiteId();
        if (siteId != null) {
            site = getSite(siteId);
            if (site != null) {
                return site;
            }
        }
        //优先根据路径查询站点
        if (StringUtils.isNotBlank(siteQueryDto.getPath())) {
            site = selectByPath(siteQueryDto.getPath());
            if (site != null) {
                return site;
            }
        }
        if (StringUtils.isNotBlank(siteQueryDto.getDomain())) {
            site = selectByDomain(siteQueryDto.getDomain());
            if (site != null) {
                return site;
            }
        }
        //如果是后台接口，管理员 根据用户的站群权限来过滤站点
        if (request.getRequestURI().startsWith(webProperties.getAdminApi().getPrefix())) {
            site = findByUserId(siteQueryDto.getUserId());
        }
        if (site == null) {
            SiteQueryReqVO reqVO = SiteQueryReqVO.builder().status(EnumIntCommonStatus.OPEN.getStatus()).build();
            List<SiteDO> siteDOS = getSiteList(reqVO);
            if (CollectionUtils.isEmpty(siteDOS)) {
                throw exception(SITE_DATA_ERROR);
            }
            site = siteDOS.get(0);
        }
        return site;
    }

    @Override
    public SiteDO getSite(String sitePath) {
        if (StringUtils.isNotBlank(sitePath)) {
            SiteDO siteDO = selectByPath(sitePath);
            if (siteDO != null) {
                return siteDO;
            }
        }
        SiteDO siteDO = CmsContextUtil.getSite();
        return siteDO;
    }

    private SiteDO selectByDomain(String domain) {
        List<SiteDO> list = siteMapper.selectSiteList(SiteQueryReqVO.builder().siteDomain(domain).build());
        return CollectionUtils.isEmpty(list) ? null : list.get(0);
    }

    private SiteDO selectByPath(String path) {
        List<SiteDO> list = siteMapper.selectSiteList(SiteQueryReqVO.builder().sitePath(path).build());
        return CollectionUtils.isEmpty(list) ? null : list.get(0);
    }

    @Override
    @LogRecord(type = CMS_SITE_TYPE, subType = CMS_SITE_TPL_UPDATE_SUB_TYPE, bizNo = "{{#siteDefTplDto.id}}", success = CMS_SITE_TPL_UPDATE_SUCCESS)
    public void updateSiteDefTplConfig(SiteDefTplDto siteDefTplDto) {
        siteTplService.createOrUpdateSiteTpl(siteDefTplDto);
        Long siteId = siteDefTplDto.getId();
        LogRecordContext.putVariable("site", getSite(siteId));
    }

    //根据用户的站群权限来过滤站点
    private SiteDO findByUserId(Long userId) {
        final LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if(loginUser==null){
            return null;
        }
        final Integer userType = loginUser.getUserType();
        if (userId == null || UserTypeEnum.MEMBER.getValue().equals(userType)) {
            return null;
        }
        final CommonResult<SitePermissionRespDTO> sitePermission = permissionApi.getSitePermission(userId);
        if (sitePermission.isError()) {
            return null;
        }
        final SitePermissionRespDTO sitePermissionData = sitePermission.getData();
        //不是全部站点的权限找到第一个满足权限的站点
        if (!sitePermissionData.getAll() && CollectionUtils.isNotEmpty(sitePermissionData.getSiteIds())) {
            return getSite(sitePermissionData.getSiteIds().iterator().next());
        }
        //继续寻找站点中的默认第一个站点
        return null;
    }

    //查询用户的默认站点,暂时先不用
    private SiteDO findDefaultSiteByUserId() {
        final LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null ) {
            return null;
        }
        final Long loginUserDefSiteId = SecurityFrameworkUtils.getLoginUserDefSiteId();
        if(loginUserDefSiteId==null){
            return null;
        }
        final CommonResult<SitePermissionRespDTO> sitePermission = permissionApi.getSitePermission(loginUser.getId());
        if (sitePermission.isError()) {
            return null;
        }
        final SitePermissionRespDTO sitePermissionData = sitePermission.getData();
        //是全部站点的权限或者用户权限包含之前设置的站点
        if (sitePermissionData.getAll() || CollectionUtils.isNotEmpty(sitePermissionData.getSiteIds())&&sitePermissionData.getSiteIds().contains(loginUserDefSiteId)) {
            return getSite(loginUserDefSiteId);
        }
        return null;
    }

    @Override
    public String generateSitePath(String sitePath) {
        final SiteDO siteDO = selectByPath(sitePath);
        if (siteDO == null) {
            return sitePath;
        }
        sitePath = sitePath + RandomUtil.randomString(4);
        return generateSitePath(sitePath);
    }

    /**
     * 通过定时任务轮询，刷新缓存
     * <p>
     * 目的：多节点部署时，通过轮询”通知“所有节点，进行刷新  2小时轮询一次，站点访问目录修改是非常低频操作
     */
    @Scheduled(initialDelay = 60, fixedRate = 2, timeUnit = TimeUnit.HOURS)
    public void refreshLocalCache() {
        // 注意：忽略自动多租户，因为要全局初始化缓存
        TenantUtils.executeIgnore(() -> {
            // 情况一：如果缓存里没有数据，则直接刷新缓存
            if (CollUtil.isEmpty(siteCache)) {
                initLocalCache();
                return;
            }

            // 情况二，如果缓存里数据，则通过 updateTime 判断是否有数据变更，有变更则刷新缓存
            LocalDateTime maxTime = getMaxValue(siteCache, SiteDO::getUpdateTime);
            if (siteMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
                initLocalCache();
            }
        });
    }

    public void initLocalCache() {
        // 第一步：查询数据
        List<SiteDO> siteDOS = siteMapper.selectList();
        log.info("[initLocalCache][缓存站点访问目录，数量为:{}]", siteDOS.size());

        // 第二步：构建缓存
        siteCache.addAll(siteDOS);
        siteVisitPathCache.addAll(com.jpro.framework.common.util.collection.CollectionUtils.convertList(siteDOS,SiteDO::getSiteVisitPath));
        CmsContextUtil.setSiteService(this);
    }

    @Override
    public Set<String> getSiteVisitPathCache() {
        return siteVisitPathCache;
    }

    @Override
    public void run(String... args) throws Exception {
        initLocalCache();
    }
}
